﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;

namespace Rosetta.Composition
{
    [DebuggerDisplay("Node {Name} (AddIn {AddIn.Name} : Method {MethodEntry.Name})")]
    public abstract class Node : IDisposable, INotifyPropertyChanged
    {
        // TODO: Load from xml file
        // TODO: Find missing argument types
        
        #region Fields

        private readonly List<NodeConnection> children = new List<NodeConnection>();
        private object _Context;
        private string _Description;
        private readonly List<NodeConnection> parents = new List<NodeConnection>();

        #endregion

        #region Events

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Constructors

        internal Node([NotNull] Template template, [NotNull] IAddIn addIn, [NotNull] string methodName)
        {
            if (template == null)
            {
                throw new ArgumentNullException("template");
            }
            if (addIn == null)
            {
                throw new ArgumentNullException("addIn");
            }
            if (string.IsNullOrEmpty(methodName))
            {
                throw new ArgumentNullException("methodName");
            }

            if (!addIn.HasMethod(methodName))
            {
                throw new ArgumentException(string.Format("Method '{0}' not found on AddIn.", methodName), "methodName");
            }

            Template = template;
            AddIn = addIn;
            MethodEntry = addIn.GetMethod(methodName);

            CanHaveChildren = addIn is IReturns;
            CanHaveContext = addIn is IInputter;
            CanHaveParents = addIn is IAccepts;
        }

        #endregion

        #region Public Properties

        [NotNull]
        public IAddIn AddIn { get; private set; }

        public int ArgumentCount
        {
            get
            {
                return CanHaveParents ? MethodEntry.ArgumentTypes.Length : 0;
            }
        }

        /// <summary>
        /// Returns true if this Node can have children.
        /// </summary>
        public bool CanHaveChildren { get; private set; }

        /// <summary>
        /// Returns true if this Node can have a context.
        /// </summary>
        public bool CanHaveContext { get; private set; }

        /// <summary>
        /// Returns true is this Node can have parents.
        /// </summary>
        public bool CanHaveParents { get; private set; }

        [NotNull]
        public IEnumerable<NodeConnection> Children
        {
            get
            {
                return children.AsReadOnly();
            }
        }

        public object Context
        {
            get
            {
                return _Context;
            }
            set
            {
                if (!CanHaveContext)
                {
                    throw new InvalidOperationException("Can't use Context with this Node.");
                }

                if (_Context != value)
                {
                    _Context = value;
                    SendPropertyChanged("Context");
                }
            }
        }

        // TODO: UI support for ContextTypes: System.String, System.IO.DirectoryInfo, System.IO.FileInfo
        public Type ContextType
        {
            get
            {
                var inputter = AddIn as IInputter;
                return inputter == null ? null : inputter.ContextType;
            }
        }

        public string Description
        {
            get
            {
                return _Description;
            }
            set
            {
                if (_Description != value)
                {
                    _Description = value;
                    SendPropertyChanged("Description");
                }
            }
        }

        [NotNull]
        public IMethodEntry MethodEntry { get; private set; }

        [NotNull]
        public int[] MissingArguments
        {
            get
            {
                if (!CanHaveParents)
                {
                    return new int[0];
                }

                var indexes = Enumerable.Range(0, ArgumentCount).ToList();
                indexes.RemoveAll(index => parents.Any(p => p.ArgumentIndex == index));
                return indexes.ToArray();
            }
        }

        public bool IsValid { get; private set; }

        [NotNull]
        public IEnumerable<NodeConnection> Parents
        {
            get
            {
                return parents.AsReadOnly();
            }
        }

        [NotNull]
        public Template Template { get; private set; }

        #endregion

        #region Public Methods

        [NotNull]
        public NodeConnection AddChild([NotNull] string addInName, [NotNull] string methodName, int argumentIndex)
        {
            if (Template == null)
            {
                throw new ObjectDisposedException("Template");
            }

            return AddChild(Template.CreateNode(addInName, methodName), argumentIndex);
        }

        [NotNull]
        public NodeConnection AddChild([NotNull] Node endPoint, int argumentIndex)
        {
            if (Template == null)
            {
                throw new ObjectDisposedException("Template");
            }

            // Check this side
            if (!CanHaveChildren)
            {
                throw new InvalidOperationException("Can't add Children to this Node.");
            }

            // Check other side
            if (endPoint == null)
            {
                throw new ArgumentNullException("endPoint");
            }
            if (endPoint == this) // TODO: Detect more recursive connections
            {
                throw new ArgumentException("Recursive connection detected!", "endPoint");
            }
            if (!endPoint.CanHaveParents)
            {
                throw new ArgumentException(string.Format("Can't add Parent to endPoint '{0}'.", endPoint.AddIn.Name), "endPoint");
            }

            // Verify if we don't have multiple parents on the same argument
            if (endPoint.Parents.Any(p => p.ArgumentIndex == argumentIndex))
            {
                throw new ArgumentException(string.Format("Connection on argument {0} already exists.", argumentIndex), "argumentIndex");
            }

            // Check combination (types)
            var argumentType = endPoint.MethodEntry.GetArgumentType(argumentIndex);
            if (MethodEntry.ReturnTypes.All(returnType => !argumentType.IsAssignableFrom(returnType)))
            {
                throw new ArgumentException(string.Format("Nodes are not compatible '{0}' -> '{1}'.", MethodEntry.ReturnType, argumentType));
            }
            bool hasMultipleResults = MethodEntry.ReturnTypes.Length > 1 && MethodEntry.ReturnType != argumentType;

            // Create connection
            var connection = new NodeConnection(this, endPoint, argumentIndex, hasMultipleResults);
            children.Add(connection);
            endPoint.parents.Add(connection);

            IsValid = MissingArguments.Length == 0;
            endPoint.IsValid = MissingArguments.Length == 0 && (!endPoint.CanHaveChildren || (endPoint.CanHaveChildren && endPoint.children.Count > 0));

            SendPropertyChanged("Children");
            SendPropertyChanged("IsValid");
            endPoint.SendPropertyChanged("Parents");
            endPoint.SendPropertyChanged("IsValid");

            return connection;
        }

        public void Dispose()
        {
// ReSharper disable AssignNullToNotNullAttribute
            AddIn = null;
            MethodEntry = null;
            Template = null;
// ReSharper restore AssignNullToNotNullAttribute

            foreach (var child in children)
            {
                child.Dispose();
            }
            children.Clear();
            parents.Clear();
        }

        #endregion

        #region Internal Methods

        internal abstract void Accept([NotNull] ITemplateVisitor visitor);

        #endregion

        #region Protected Methods

        protected void SendPropertyChanged([NotNull] string propertyName)
        {
            var propertyChanged = PropertyChanged;
            if (propertyChanged != null)
            {
                propertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion
    }
}